package roborally.program.text;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

import roborally.program.CommandManager;
import roborally.program.command.BasicCondition;
import roborally.program.command.Command;
import roborally.program.command.ComplexCondition;
import roborally.program.command.Condition;
import roborally.program.command.IfCommand;
import roborally.program.command.WhileCommand;
import roborally.program.command.ComplexCondition.ConditionCombinationProcedure;
import roborally.program.command.ExecutionCommand;
import roborally.program.command.SequenceCommand;
import roborally.program.command.BasicCondition.ConditionProcedure;
import roborally.program.command.ExecutionCommand.ExecutionProcedure;

/**
 * This enum contains the different factories for creating the corresponding commands. 
 * A factory is responsible for searching and initializing the possible valid patterns
 * of the class of which the factory creates objects.
 * Each factory returns an anonymous class, implementing the interface TextCommandPattern.
 * Using that interface, the right java command objects can be created.
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 1.0
 * @note We assume that some abstract command as String input always starts and finishes with a bracket.
 *
 */
public enum TextFactory {
	
	/**
	 * The factory execution returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class ExecutionCommand. Depending on the input, the right ExecutionProcedure will be added. The object
	 * is also responsible for collecting the patterns according the different ExecutionProcedures.
	 */
	EXECUTION{

		/**
		 * A Map containing the different possible ExecutionProcedures and their corresponding String pattern
		 * as key.
		 */
		private Map<String, ExecutionProcedure> patterns;
		
		/**
		 * This method searches for all the ExecutionProcedures specified in the class
		 * ExecutionCommand and initializes the patterns map.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post This will contain a map with as key the pattern of the command and as value
		 * the procedure describing the command, and that for all the ExecutionProcedures specified in the class
		 * ExecutionCommand.
		 */
		@Override @Raw
		protected void setUp() {
			this.setSetUp();
			patterns = new HashMap<String, ExecutionProcedure>();
			for(ExecutionProcedure lusProc : ExecutionProcedure.values())
				patterns.put(lusProc.getStringPattern(), lusProc);
		}
		
		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * ExecutionCommands from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * ExecutionCommands from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert this.getSetUp();
			
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches one of the possible patterns for
				 * creating an ExecutionCommand with corresponding ExecutionProcedure.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 */
				@Override
				public boolean matches(String word) {
					return TextFactory.EXECUTION.matches(word, patterns);
				}
				
				/**
				 * Creates a new ExecutionCommand from the given String input and with
				 * the according ExecutionProcedure.
				 * 
				 * @param word The word of which an ExecutionCommand must be created.
				 * @return A new ExecutionCommand which a corresponding ExecutionProcedure
				 * according the given word.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					word = TextManipulation.removeRedundantSymbols(word);
					for(String pattern : patterns.keySet())
						if(Pattern.matches(pattern, word)) {
							ExecutionProcedure proc = patterns.get(pattern);
							return new ExecutionCommand(proc, TextFactory.EXECUTION.getArguments(proc, word));
						}
					throw new IllegalArgumentException("Word is not valid for an executionProcedure.\n" + word);
				}
			};
		}
	},
	
	/**
	 * The factory sequence returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class SequenceCommands. Depending on the input, the right internal Commands are added by invoking 
	 * recursively the create method from the CommandManager class.
	 */
	SEQUENCE {

		/**
		 * The String pattern of which a valid SequenceCommand can be created.
		 */
		private String pattern;
		
		/**
		 * This method initializes the internal String pattern for creating valid
		 * SequenceCommands.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post The internal pattern for creating valid SequenceCommands will be
		 * initialized.
		 */
		@Override @Raw
		protected void setUp() {
			this.setSetUp();
			pattern = "^(\\()(\\s*)(" + SequenceCommand.getWord() + ")(.*)(\\))";	
			//  new line   (  [   ] seq ... ) with [  ] a random number white spaces.
		}
		
		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * SequenceCommands from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * SequenceCommands from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert getSetUp();
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches the pattern for
				 * creating an SequenceCommand with corresponding nested 
				 * internal Commands.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 * @note We assume that in case of a nested command, the inner
				 * commands are correct.
				 */
				@Override
				public boolean matches(String word) {
					word = TextManipulation.removeRedundantSymbols(word);
					return Pattern.matches(pattern, word);
				}
				
				/**
				 * Creates a new SequenceCommand from the given String input with
				 * the according nested internal Commands.
				 * 
				 * @param word The word of which an SequenceCommand must be created.
				 * @return A new SequenceCommand containing the nested internal commands
				 * the given word may specify.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 * @throws IllegalArgumentException
				 * 		When some of the nested internal Commands is not valid according
				 * 		their specific matches method.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					if(matches(word)) {
						word = word.replaceFirst("^(\\()(\\s*)(" + SequenceCommand.getWord() + ")", ""); // removes the new line ([   ]seq
						word = word.substring(0, word.length() - 1).trim(); // removes the ) and beginning and tailing white spaces
						return new SequenceCommand(CommandManager.get().create(word));
					}
					throw new IllegalArgumentException("Word is not valid for sequence. \n" + word);
				}
			};
		}

	}, 
	
	/**
	 * The factory basic-condition returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class BasicCondition.
	 */
	BASIC_CONDITION {

		/**
		 * A Map containing the different possible ConditionProcedures and their corresponding String pattern
		 * as key.
		 */
		private Map<String, ConditionProcedure> patterns;
		
		/**
		 * This method searches for all the ConditionProcedures specified in the class
		 * BasicCondition and initializes the patterns map.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post This will contain all the ConditionProcedures specified in the class
		 * BasicCondition.
		 */
		@Override @Raw
		protected void setUp() {
			this.setSetUp();
			patterns = new HashMap<String, ConditionProcedure>();
			for(ConditionProcedure lusProc : ConditionProcedure.values())
				patterns.put(lusProc.getStringPattern(), lusProc);			
		}

		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * BasicConditions from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * BasicConditions from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert getSetUp();
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches one of the possible patterns for
				 * creating a BasicCondition with corresponding ConditionProcedure.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 */
				@Override
				public boolean matches(String word) {
					return TextFactory.BASIC_CONDITION.matches(word, patterns);
				}
				
				/**
				 * Creates a new BasicCondition from the given String input and with
				 * the according ConditionProcedure.
				 * 
				 * @param word The word of which an BasicCondition must be created.
				 * @return A new BasicCondition which a corresponding ConditionProcedure
				 * according the given word.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					word = TextManipulation.removeRedundantSymbols(word);
					for(String pattern : patterns.keySet())
						if(Pattern.matches(pattern, word)) {
							word = word.replaceFirst("^(\\()(\\s*)([\\w]+)", "");
							word = word.substring(0, word.length() - 1);
							ConditionProcedure proc = patterns.get(pattern);
							return new BasicCondition(proc, TextFactory.BASIC_CONDITION.getArguments(proc, word));
						}
					throw new IllegalArgumentException("Unable to create a basic condition with the given word: " + word);
				}
			};
		}
		
	},
	
	/**
	 * The factory complex-condition returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class ComplexCondition. The right ConditionCombinationProcedure is added together with the required parameters.
	 */
	COMPLEX_CONDITION {

		/**
		 * A Map containing the different possible ConditionCombinationProcedures and their corresponding String pattern
		 * as key.
		 */
		private Map<String, ConditionCombinationProcedure> patterns;
		
		/**
		 * This method searches for all the ConditionCombinationProcedures specified in the class
		 * ComplexCondition and initializes the patterns map.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post This will contain all the ConditionCombinationProcedures specified in the class
		 * ComplexCondition.
		 */
		@Override
		protected void setUp() {
			this.setSetUp();
			patterns = new HashMap<String, ConditionCombinationProcedure>();
			for(ConditionCombinationProcedure lusProc : ConditionCombinationProcedure.values())
				patterns.put(lusProc.getStringPattern(), lusProc);
		}
		
		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * ComplexConditions from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * ComplexConditions from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert getSetUp();
			
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches one of the possible patterns for
				 * creating a ComplexCondition with corresponding ConditionCombinationProcedure.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 */
				@Override
				public boolean matches(String word) {
					return TextFactory.COMPLEX_CONDITION.matches(word, patterns);
				}
				
				/**
				 * Creates a new ComplexCondition from the given String input with
				 * the according ConditionCombinationProcedure and the corresponding
				 * parameters.
				 * 
				 * @param word The word of which an ComplexCondition must be created.
				 * @return A new ComplexCondition containing a ConditionCombinationProcedure
				 * and the corresponding parameters.
				 * the given word may specify.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 * @throws IllegalArgumentException
				 * 		When some of the nested internal Commands are not valid according
				 * 		their specific matches method. For example, the matches method of this
				 * 		controls if there is syntactical a condition specified. But when 
				 * 		that condition is wrong specified, say a wrong name, then the matches method
				 * 		for creating a condition, invoked in this method, 
				 * 		will throw an exception. This method will pass that exception.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					word = TextManipulation.removeRedundantSymbols(word);
					for(String pattern : patterns.keySet())
						if(Pattern.matches(pattern, word)) {
							ConditionCombinationProcedure proc = patterns.get(pattern);
							word = word.replaceFirst("^(\\()(\\s*)(" + proc.toString() + ")", "");
							word = word.substring(0, word.length() - 1).trim();
							List<String> subBlocks = TextManipulation.getBlocks(word); // will return a list containing the different parts of the complex condition
							List<Object> args = new ArrayList<Object>();
							for(String lusBlock : subBlocks)
								args.add(proc.getValue(lusBlock));
							return new ComplexCondition(proc, args.toArray());
						}
					throw new IllegalArgumentException("Unable to create a complex condition with the given word: " + word);
				}
			};
		}

		
	}, 
	
	/**
	 * The factory while returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class WhileCommands. Depending on the input, the right internal Commands are added by invoking 
	 * recursively the create method from the CommandManager class.
	 */
	WHILE {

		/**
		 * The String pattern of which a valid WhileCommand can be created.
		 */
		private String pattern;
		
		/**
		 * This method initializes the internal String pattern for creating valid
		 * WhileCommands.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post The internal pattern for creating valid WhileCommands will be
		 * initialized.
		 */
		@Override @Raw
		protected void setUp() {
			this.setSetUp();
			// new line( [  ] while [  ] 
			pattern = "^(\\()(\\s*)(" + WhileCommand.getWord() + ")(\\s*)" +
					"(" +
						// [  ] ( 
						// 	OR [  ] not{ ( or ) } ( ... ) [  ] 
						// 	OR not{ ( or ) }
						// [  ] ) [  ]
						// the previous two times
						"(\\s*)(\\()" + "(((\\s*)([^\\(\\)]+)(\\()(.*)(\\))(\\s*))|([^\\(\\)]*))" + "(\\s*)(\\))(\\s*)" +
					"){2}" +
						// [  ] )
					"(\\s*)(\\))";
		}
		
		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * WhileCommands from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * WhileCommands from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert getSetUp();
			
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches one of the possible patterns for
				 * creating an WhileCommand with a condition and corresponding nested 
				 * internal Commands.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 */
				@Override
				public boolean matches(String word) {
					word = TextManipulation.removeRedundantSymbols(word);
					return Pattern.matches(pattern, word);
				}
				
				/**
				 * Creates a new WhileCommand from the given String input with a Condition and
				 * the according nested internal commands.
				 * 
				 * @param word The word of which an WhileCondition must be created.
				 * @return A new WhileCondition containing a Condition 
				 * and the according nested internal commands the given word may specify.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 * @throws IllegalArgumentException
				 * 		When some of the nested internal Commands are not valid according
				 * 		their specific matches method. For example, the matches method of this
				 * 		controls if their is syntactical a condition specified. But when 
				 * 		that condition is wrong specified, say a wrong name, then the matches method
				 * 		for creating a condition, invoked in this method, 
				 * 		will throw an exception. This method will pass that exception.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					word = word.replaceFirst("^(\\()(\\s*)(" + WhileCommand.getWord() + ")", "");
					word = word.substring(0, word.length() - 1).trim();
					List<String> blocks = TextManipulation.getBlocks(word);
					if(blocks.size() == 2){
						if (! Condition.class.isInstance(CommandManager.get().createCommand(blocks.get(0))))
							throw new IllegalArgumentException("The first argument of while should be a condition");
						return new WhileCommand(Condition.class.cast(CommandManager.get().createCommand(blocks.get(0))),
								CommandManager.get().create(blocks.get(1)));
					}
					throw new IllegalArgumentException("Could not create a while command with the given word: " + word);
				}
			};
		}
		
	}, 
	
	/**
	 * The factory if returns a specification for the TextCommandPattern interface for creating new objects of 
	 * the class IfCommands. Depending on the input, the right internal Commands are added by invoking 
	 * recursively the create method from the CommandManager class.
	 */
	IF {

		/**
		 * The String pattern of which a valid IfCommand can be created.
		 */
		private String pattern;
		
		/**
		 * This method initializes the internal String pattern for creating valid
		 * IfCommands.
		 * 
		 * @post This will be set up.
		 * 		| this.getSetUp() == true
		 * @post The internal pattern for creating valid IfCommands will be
		 * initialized.
		 */
		@Override @Raw
		protected void setUp() {
			this.setSetUp();
			pattern = "^(\\()(\\s*)(" + IfCommand.getWord() + ")(\\s*)" +
					"(" + // see while, but three times in stead of two times
						"(\\s*)(\\()" + "(((\\s*)([^\\(\\)]+)(\\()(.*)(\\))(\\s*))|([^\\(\\)]*))" + "(\\s*)(\\))(\\s*)" +
					"){3}" +
					"(\\s*)(\\))";
		}
		
		/**
		 * Returns an implementation for the TextCommandPattern interface for creating
		 * IfCommands from String input.
		 * 
		 * @pre This must be set up.
		 * 		| this.getSetUp()
		 * @return An implementation for the TextCommandPattern interface for creating
		 * IfCommands from String input.
		 */
		@Override
		public TextCommandPattern getPattern() {
			assert this.getSetUp();
			
			return new TextCommandPattern() {
				
				/**
				 * Controls if the given word matches one of the possible patterns for
				 * creating an IfCommand with a condition, nested 
				 * internal Commands and nested internal false Commands.
				 * 
				 * @param word The word to be controlled.
				 * @return True if the given word matches a valid pattern.
				 */
				@Override
				public boolean matches(String word) {
					word = TextManipulation.removeRedundantSymbols(word);
					return Pattern.matches(pattern, word);
				}
				
				/**
				 * Creates a new IfCommand from the given String input with a Condition,
				 * the according nested internal commands and the according nested false
				 * internal commands.
				 * 
				 * @param word The word of which an IfCommand must be created.
				 * @return A new IfCommand containing a Condition, the according 
				 * nested internal commands and the according nested false internal
				 * commands the given word may specify.
				 * @throws IllegalArgumentException
				 * 		When the given word is not valid according the matches method.
				 * @throws IllegalArgumentException
				 * 		When some of the nested internal Commands are not valid according
				 * 		their specific matches method. For example, the matches method of this
				 * 		controls if their is syntactical a condition specified. But when 
				 * 		that condition is false specified, say a wrong name, then the matches method
				 * 		for creating a condition, invoked in this method, 
				 * 		will throw an exception. This method will pass that exception.
				 */
				@Override
				public Command create(String word) throws IllegalArgumentException {
					word = word.replaceFirst("^(\\()(\\s*)(" + IfCommand.getWord() + ")", "");
					word = word.substring(0, word.length() - 1).trim();
					List<String> blocks = TextManipulation.getBlocks(word);
					if(blocks.size() == 3){
						if (! Condition.class.isInstance(CommandManager.get().createCommand(blocks.get(0))))
							throw new IllegalArgumentException("The first argument of if should be a condition");
						return new IfCommand(Condition.class.cast(CommandManager.get().createCommand(blocks.get(0))), 
								CommandManager.get().create(blocks.get(1)), CommandManager.get().create(blocks.get(2)) );
					}
					throw new IllegalArgumentException("Could not create a while command with the given word: " + word);
				}
			};
		}

		
		
	};
	
	/**
	 * A constructor for creating TextFactories. The constructor is used
	 * to guarantee that all the internal aspects are properly initialized
	 * before invoking one or more methods.
	 * 
	 * @effect The method setUp() is been invoked.
	 * 		| setUp()
	 */
	private TextFactory() {
		this.setUp();
	}
	
	/**
	 * Returns whether or not this TextFactory is been set up.
	 */
	@Basic @Raw
	public boolean getSetUp() {
		return this.isSetUp;
	}

	/**
	 * Sets this setUp to be true.
	 * 
	 * @post This will be set up.
	 * 		| getSetUp() == true.
	 * @note This method is and auxiliary method for the total method
	 * setUp() that is different for each factory (for each enum-literal). 
	 * It is not recommended to use this method somewhere else.
	 */
	protected void setSetUp() {
		this.isSetUp = true;
	}
	
	/**
	 * This method initializes all the internal aspects of the text factories
	 * in order to be able to return the right implementation of the TextCommandPattern
	 * interface for creating Commands from text input.
	 * 
	 * @post This will be set up after invoking this method.
	 * 		| this.getSetUp() == true
	 */
	@Raw
	protected abstract void setUp();
	
	/**
	 * A variable for controlling if the internal aspects of this are set up.
	 */
	private boolean isSetUp = false;
	
	/**
	 * Returns the TextCommandPattern of this TextFactory. The given implementation
	 * is responsible for controlling and creating the actual objects from
	 * the given String input.
	 */
	@Basic
	public abstract TextCommandPattern getPattern();

	/**
	 * Controls if the given word matches some String Pattern as key in the map.
	 * 
	 * @param word The word for which a valid match must be searched.
	 * @param patterns The map with key a String used as Pattern. The corresponding
	 * value is not used.
	 * @return True if the word matches some String Pattern as key. Otherwise false.
	 */
	private boolean matches(String word, Map<String, ?> patterns) {
		word = TextManipulation.removeRedundantSymbols(word);
		for(String pattern : patterns.keySet())
			if(Pattern.matches(pattern, word))
				return true;
		return false;
	}
	
	/**
	 * Returns a array of Objects that are the arguments corresponding 
	 * the given TextProcedure.
	 * 
	 * @param proc The textProdecure of which the arguments must be created.
	 * @param word The String representation of the arguments that are required.
	 * @return An array containing all the values of arguments the given TextProcedure
	 * requires. 
	 * @throws IllegalArgumentException
	 * 		When the given String is not valid for creating the values of the needed
	 * 		arguments of the given TextProcedure.
	 */
	private Object[] getArguments(TextProcedure proc,
			String word) throws IllegalArgumentException {
		List<Object> args = new ArrayList<Object>();
		String[] subword = word.replaceAll("\\(", " ").replaceAll("\\)"," ").trim().split("\\s++"); // replace ( and ) by white space, trims and splits at one or more white spaces
		for(int i = 1 ; i < subword.length; i++) // (trim removes the leading and trailing white spaces)
			args.add(proc.getValue(subword[i])); // wrong arguments are detected at getValue()
		return args.toArray();
	}

}
